package lelouet.sh3d.plugins.sweetDatacenter.model;

import entropy.configuration.Configuration;
import entropy.configuration.Node;
import entropy.configuration.VirtualMachine;
import entropy.configuration.parser.ConfigurationParser;
import entropy.configuration.parser.ParseException;
import gridlib.AllFactories;
import gridlib.api.Cluster;
import gridlib.api.ConfigurationException;
import gridlib.api.Grid;
import gridlib.api.GridElement;
import gridlib.api.GridElementFactory;
import gridlib.api.GridFactory;
import gridlib.api.IllegalLifeCycleException;
import gridlib.api.Level;
import gridlib.api.Machine;
import gridlib.api.MachineModel;
import gridlib.api.Rack;
import gridlib.api.Site;
import gridlib.api.VM;
import gridlib.api.GridElementFactory.VMElementType;
import gridlib.api.MachineLifeCycleController.MachineLifeCycleState;
import gridlib.authentification.BasicAuthentificationManager;
import gridlib.authentification.Session;
import gridlib.parser.GridConfigurationParser;

import java.io.File;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import lelouet.sh3d.Utils;
import lelouet.sh3d.plugins.sweetDatacenter.SweetDatacenterPlugin;

import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
import com.eteks.sweethome3d.model.FurnitureCatalog;
import com.eteks.sweethome3d.model.FurnitureCategory;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;

/**
 * a class that links {@link GElement} and {@link GridElement}.<br />
 * It loads a model catalog, and then try to guess what model fit the best to
 * each {@link Rack} or {@link Machine}<br />
 * It is used for loading models, copying.
 */
public class GElementFactory extends GridGraphicalBridge {

	static String indent[] = { "", " ", "  ", "   ", "    " };

	public static String indent(final int i) {
		return GElementFactory.indent[i];
	}

	/** factory to generate the Grid and its elements */
	protected final GridFactory gridFactory;

	/** the factory to generate the grid elements(rack and server) */
	protected GridElementFactory geFactory;

	protected Session session;

	private static final long serialVersionUID = 1L;

	/**
	 * the catalog of graphic model we can use. when loading a new server
	 * graphic model, it is added in it as a {@link CatalogPieceOfFurniture}
	 */
	protected FurnitureCatalog catalog = null;

	/** the id of the default 1U server model */
	protected String default1UId = Utils.getDefaultServerId(1);

	/** the default {@link Cluster} in which to add the created racks */
	protected Cluster defaultCluster = null;

	/** the id of the default rack model */
	protected String defaultRackId = "defaultRack";

	/** the default model of a rack */
	protected CatalogPieceOfFurniture defaultRackModel = null;

	/** the default model of a server of size 1U */
	protected CatalogPieceOfFurniture defaultServer1UModel = null;

	/** for each eight (in U), the default model of a server of that size */
	protected Map<Integer, CatalogPieceOfFurniture> UModels = new HashMap<Integer, CatalogPieceOfFurniture>();

	/**
	 * create a factory that has a catalog. This adds the default 1..1O U server
	 * models to that catalog
	 */
	public GElementFactory(final FurnitureCatalog catalog, final Home home) {
		super(home);
		try {
			gridFactory = AllFactories.getGridFactory();
			session = AllFactories.getAuthentificationManager().login(
					BasicAuthentificationManager.ADMIN_NAME,
					BasicAuthentificationManager.ADMIN_PASSWD);
			grid = gridFactory.newGrid(session, "anonymousGrid");
			geFactory = gridFactory.getGridEltFactory(grid);
			this.catalog = catalog;
			updateFromCatalog();
			createDefaultCluster();
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	@Override
	public void clear() {
		super.clear();
		try {
			Utils.removeCategory(catalog, SweetDatacenterPlugin.MODEL_CATEGORY);
			gridFactory.clearGlobals();
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
		}
	}

	protected GRack convertElementToRack(HomePieceOfFurniture furniture) {
		try {
			Rack rack = geFactory.newRack(null);
			registerNewRack(rack);
			GRack elem = new GRack(this, furniture, rack);
			gracks.add(elem);
			return elem;
		} catch (Exception e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/** create a {@link GServer} from a furniture representing one */
	protected GServer convertElementToServer(HomePieceOfFurniture furniture) {
		try {
			Machine server = geFactory.newMachine(null);
			String name = furniture.getName();
			MachineModel model = grid.getModelByName(name);
			if (model != null) {
				server.setModel(model);
			} else {
				int lvlNb = (int) Math
						.ceil(furniture.getHeight()
								/ lelouet.sh3d.plugins.sweetDatacenter.model.Utils.U_IN_CM);
				server.setVSize(lvlNb);
				server.setCpuArch(64.0);
				server.setCpuNumber(4.0);
				server.setCpuCapacity(3000.0);
				server.setMemory(4000.0);
			}
			server.setMac(createCorrectServerMac());
			server.setIp(createCorrectServerIp());
			server.setOsFamily("Unix");
			server.setOsDistrib("ubuntu");
			GServer elem = new GServer(this, furniture, server);
			gservers.add(elem);
			return elem;
		} catch (Exception e) {
			e.printStackTrace(System.err);
			return null;
		}
	}

	protected String createCorrectServerMac() {
		StringBuilder buffer = null;
		for (int i = 1; i < 254; i++) {
			for (int j = 0; j < 254; j++) {
				buffer = new StringBuilder();
				buffer.append("00:00:00:00:");
				String firstString = Integer.toHexString(i);
				for (int ii = firstString.length(); ii < 2; ii++) {
					buffer.append('0');
				}
				buffer.append(firstString);
				buffer.append(':');
				String secondString = Integer.toHexString(j);
				for (int ii = secondString.length(); ii < 2; ii++) {
					buffer.append('0');
				}
				buffer.append(secondString);
				String adress = buffer.toString();
				try {
					if (!geFactory.existMachineMac(adress)) {
						return adress;
					}
				} catch (RemoteException e) {
					throw new RuntimeException(
							"TODO: Handle this exception better", e);
				}
			}
		}
		throw new RuntimeException("could not get a correct mac adress");
	}

	protected String createCorrectServerIp() {
		StringBuilder buffer = null;
		for (int i = 0; i < 254; i++) {
			for (int j = 1; j < 254; j++) {
				buffer = new StringBuilder();
				buffer.append("192.168.");
				buffer.append(i);
				buffer.append('.');
				buffer.append(j);
				String ip = buffer.toString();
				try {
					if (!geFactory.existMachineIp(ip)) {
						return ip;
					}
				} catch (RemoteException e) {
					throw new RuntimeException(
							"TODO: Handle this exception better", e);
				}
			}
		}
		throw new RuntimeException("could not get a correct mac adress");
	}

	/**
	 * generate a {@link GElement} from a piece of furniture
	 * 
	 * @param furniture
	 *            a piece of furniture that IS not yet a {@link GElement}
	 * @return an element already added to the {@link Grid} model, but not to
	 *         the graphical one
	 */

	public GElement convertElement(final HomePieceOfFurniture furniture) {
		final String description = furniture.getDescription();
		if (description != null && description.toLowerCase().startsWith("rack")) {
			return convertElementToRack(furniture);
		} else {
			GServer server = convertElementToServer(furniture);
			return server;
		}
	}

	/**
	 * initialize a new cluster to be the default one, if none is yet existing.
	 */
	protected void createDefaultCluster() {
		try {
			if (defaultCluster == null) {
				Site defaultSite = null;
				if (grid.getSites().isEmpty()) {
					Site site = geFactory.newSite("defaultSite");
					site.setCity("defaultCity");
					grid.addSite(site);
					defaultSite = site;
				} else {
					defaultSite = (Site) grid.getSites().toArray()[0];
				}
				defaultCluster = geFactory.newCluster("defaultCluster");
				defaultSite.addCluster(defaultCluster);
			}
		} catch (Exception e) {
			throw new RuntimeException("handle this better", e);
		}
	}

	/** generates a new {@link Rack} from default model */
	protected Rack generateDefaultRack() {
		try {
			return geFactory.newRack(null);
		} catch (final Exception e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/**
	 * generates a {@link GRack} for given {@link Rack} <br />
	 * <ul>
	 * <li>guess what is the best model for that rack</li>
	 * <li>If no model is given, or the model is not correct, then we use the
	 * default model of standard rack</li>
	 * <li>the resulting GRack is added to the Home.</li>
	 * <li>the position of the {@link GRack} is this of the given {@link Rack}
	 * if there was no rack there yet, or it is moved somewhere else</li>
	 * </ul>
	 * 
	 * @param rack
	 *            a rack that is used as the model for the Grack, and the
	 *            {@link GridElement} it is linked to
	 * @return a {@link GRack} based on given {@link Rack}, or null
	 */

	public GRack generateGRack(Rack rack) {
		if (rack == null) {
			rack = generateDefaultRack();
		}
		final GRack newRack = new GRack(this, defaultRackModel, rack);
		try {
			newRack.setName(rack.getName());
			newRack.setAngle(rack.getAngle());
			newRack.setX(rack.getPosX());
			newRack.setY(rack.getPosY());
			newRack.setElevation(rack.getElevation());
		} catch (final RemoteException e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
		gracks.add(newRack);
		getHome().addPieceOfFurniture(newRack);
		placeGRack(newRack);
		return newRack;
	}

	/**
	 * generates a {@link GServer} from a {@link Machine}.<br />
	 * If there is no correct model for that machine, then the
	 * {@link #defaultServer1UModel} is used<br />
	 * The created server is added to the home
	 */

	public GServer generateGServer(final Machine machine) {
		CatalogPieceOfFurniture serverModel = findServerGModel(machine);
		final GServer server = new GServer(this, serverModel, machine);
		gservers.add(server);
		try {
			Level level = machine.getLevel();
			if (level != null) {
				server
						.setElevation(level.getNumber()
								* lelouet.sh3d.plugins.sweetDatacenter.model.Utils.U_IN_CM);
				Rack rack = level.getRack();
				if (rack != null) {
					server.setAngle(rack.getAngle());
					server.setElevation(server.getElevation()
							+ rack.getElevation());
				}
			}
		} catch (RemoteException e) {
			e.printStackTrace();
		}
		getHome().addPieceOfFurniture(server);
		return server;
	}

	protected CatalogPieceOfFurniture findServerGModel(Machine machine) {
		try {
			return getModelU(machine.getVSize());
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
		}
		return defaultServer1UModel;
	}

	/**
	 * select any cluster of the grid as the default cluster
	 * 
	 * @param grid
	 */
	protected void getDefaultCluster(final Grid grid) {
		defaultCluster = null;
		try {
			for (final Site site : grid.getSites()) {
				for (final Cluster cluster : site.getClusters()) {
					defaultCluster = cluster;
					return;
				}
			}
		} catch (Exception e) {
			e.printStackTrace(System.err);
		}
	}

	/**
	 * returns a model for given server size ; If cached, return the cached one;
	 * If not cached yet, get it from the catalog. If not in the catalog,
	 * creates it from the basic 1U server and add it to the catalog.
	 * 
	 * @return a model for the given height of a server
	 * @param uHeight
	 *            the height, in U unit(4.45 cm) of the server we want
	 * @see <a href="http://en.wikipedia.org/wiki/Rack_unit">U description on
	 *      wikipedia</a>
	 */
	protected CatalogPieceOfFurniture getModelU(int uHeight) {
		if (uHeight < 1) {
			return getModelU(1);
		}
		CatalogPieceOfFurniture model = UModels.get(uHeight);
		if (model == null) {
			final String sid = Utils.getDefaultServerId(uHeight);
			final String sname = "default" + (uHeight < 10 ? "0" : "")
					+ uHeight + "U";
			model = Utils.getFurnitureFromId(catalog, sid);
			if (model == null) {
				if (defaultServer1UModel == null) {
					return null;
				}
				model = new CatalogPieceOfFurniture(sid, sname, sid,
						defaultServer1UModel.getIcon(), defaultServer1UModel
								.getPlanIcon(),
						defaultServer1UModel.getModel(), defaultServer1UModel
								.getWidth(), defaultServer1UModel.getDepth(),
						defaultServer1UModel.getHeight() * uHeight,
						defaultServer1UModel.getElevation(),
						defaultServer1UModel.isMovable(), defaultServer1UModel
								.getModelRotation(), defaultServer1UModel
								.getCreator(), defaultServer1UModel
								.isResizable(),
						defaultServer1UModel.getPrice(), defaultServer1UModel
								.getValueAddedTaxPercentage());
			}
			UModels.put(uHeight, model);
		}
		return model;
	}

	/**
	 * what to do when a new rack has just been created and has to be registered
	 * in the existing grid
	 * 
	 * @param rack
	 *            the just created rack
	 */
	protected void registerNewRack(final Rack rack) {
		if (defaultCluster != null) {
			try {
				defaultCluster.addRack(rack);
			} catch (final Exception e) {
				e.printStackTrace(System.err);
			}
		}
	}

	/**
	 * guess what are the best model to use as default for {@link Rack}s and
	 * {@link Machine}s
	 */
	protected void selectDefaultModels() {
		for (final FurnitureCategory fc : catalog.getCategories()) {
			System.out.println("category " + fc.getName());
			for (final CatalogPieceOfFurniture cpof : fc.getFurniture()) {
				System.out.println("  - " + cpof.getName() + " , id : "
						+ cpof.getId());
				if (defaultServer1UModel == null || cpof.getId() != null
						&& cpof.getId().equals(default1UId)) {
					defaultServer1UModel = cpof;
				}
				if (defaultRackModel == null || cpof.getId() != null
						&& cpof.getId().equals(defaultRackId)) {
					defaultRackModel = cpof;
				}
			}
		}
		UModels.put(1, defaultServer1UModel);
	}

	/** replace the grid we use by another */
	public void setGrid(final Grid grid) {
		this.grid = grid;
		try {
			geFactory = gridFactory.getGridEltFactory(grid);
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
		}
		getDefaultCluster(grid);
		createDefaultCluster();
		updateHomeFromGrid();
	}

	/** updates the preferences and the cache when the catalog has been changed */
	protected void updateFromCatalog() {
		selectDefaultModels();
		UModels.clear();
		getModelU(1);
	}

	/** change the home we work in */
	protected void setHome(final Home home) {
		this.home = home;
		updateGridFromHome();
	}

	/** not obviously useful */
	protected void updateGridFromHome() {
		throw new UnsupportedOperationException("TODO: Implement this method");
	}

	protected void updateHomeFromGrid() {
		try {
			for (final Site site : grid.getSites()) {
				for (final Cluster cluster : site.getClusters()) {
					for (final Rack rack : cluster.getRacks()) {
						final GRack newRack = generateGRack(rack);
						for (final Level level : rack.getLevels()) {
							final GServer newMachine = generateGServer(level
									.getMachine());
							newMachine.removeFromRack();
							newRack
									.addServer(newMachine,
											level.getNumber() - 1);
						}
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/**
	 * parse a Grid xml file to generate in the home AND the grid model the
	 * corresponding elements
	 */
	public void parseGridlib(final File file) {
		try {
			final GridConfigurationParser parser = new GridConfigurationParser(
					gridFactory);
			Session session = AllFactories.getAuthentificationManager().login(
					BasicAuthentificationManager.ADMIN_NAME,
					BasicAuthentificationManager.ADMIN_PASSWD);
			Grid parsedGrid = parser.parse(session, file.getAbsolutePath());
			setGrid(parsedGrid);
			// for(Object o : gridFactory.)
		} catch (final Exception e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/**
	 * parse a file and try to import it as an entropy file. It removes every
	 * attached vms to servers
	 */
	public void parseEntropy(File file) {
		try {
			Configuration conf = ConfigurationParser.parseConfiguration(file
					.getAbsolutePath());
			// first, deallocate every VM from their machine
			/** list of vm to remove from iterated machine */
			ArrayList<VM> toRemove = new ArrayList<VM>(20);
			for (Machine machine : grid.getAllMachines()) {
				toRemove.clear();
				for (VM vm : machine.getVMs()) {
					toRemove.add(vm);
				}
				for (VM vm : toRemove) {
					try {
						machine.removeVM(vm);
					} catch (ConfigurationException e) {
						e.printStackTrace(System.err);
					}
				}
			}
			for (Node node : conf.getOnlines()) {
				System.out.println("applying to node " + node.getName());
				Machine gridmachine = null;
				for (GServer gserver : gservers) {
					if (gserver.getName().equals(node.getName())) {
						gridmachine = gserver.getGridElement();
						break;
					}
				}
				if (gridmachine == null) {
					System.err
							.println("  error : no machine with name "
									+ node.getName()
									+ " in the grid ; could not fully load entropy file");
				} else {
					try {
						boolean machineStateWasOn = gridmachine.getState() == MachineLifeCycleState.ON;
						if (machineStateWasOn) {
							gridmachine.setState(MachineLifeCycleState.OFF);
						}
						double cpuMult = (double) gridmachine.getCpuCapacity()
								/ (node.getCPUCapacity() * node.getNbOfCPUs());
						double memMult = (double) gridmachine.getMemory()
								/ node.getMemoryTotal();
						for (VirtualMachine vm : conf.getRunnings(node)) {
							VM gridVM = (VM) geFactory.getInGlobals(
									VMElementType.VM, vm.getName());
							if (gridVM == null) {
								System.out.println("no VM with name "
										+ vm.getName()
										+ " in the grid; trying to create it");
								gridVM = convertVirtualMachine(vm);
							}
							if (gridVM != null) {
								gridVM
										.setConsCpu(gridVM.getConsCpu()
												* cpuMult);
								gridVM.setConsMemory(gridVM.getConsMemory()
										* memMult);
								if (gridVM.getMachine() != null) {
									gridVM.getMachine().removeVM(gridVM);
								}
								gridmachine.addVM(gridVM);
							} else {
								System.err
										.println("could not get a VM with name "
												+ vm.getName());
							}
						}
						if (machineStateWasOn) {
							gridmachine.setState(MachineLifeCycleState.ON);
						}
					} catch (IllegalLifeCycleException e) {
						e.printStackTrace();
					} catch (ConfigurationException e) {
						e.printStackTrace();
					}
				}
			}
		} catch (ParseException e) {
			e.printStackTrace(System.err);
		} catch (IOException e) {
			e.printStackTrace(System.err);
		}
	}

	/** generates a {@link VM} from a {@link VirtualMachine} */
	protected VM convertVirtualMachine(VirtualMachine vm) {
		try {
			VM ret = geFactory.newVM(vm.getName());
			ret.alreadyRun();
			ret.setConsCpu(vm.getCPUConsumption());
			ret.setConsMemory(vm.getMemoryConsumption());
			return ret;
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
		} catch (ConfigurationException e) {
			e.printStackTrace(System.err);
		} catch (IllegalLifeCycleException e) {
			e.printStackTrace(System.err);
		}
		return null;
	}

	/** @return the base name, without any final _X, X being a number */
	protected static String getBaseName(String name) {
		if (name.matches("^.*_[0-9]+$")) {
			return name.replaceAll("_[0-9]+$", "");
		} else {
			return name;
		}
	}

	protected static String numberedName(String baseName, int number) {
		if (number == 0) {
			return baseName;
		}
		return baseName + '_' + number;
	}

	/**
	 * chose a name for given element. The name given is baseName if it is
	 * available, or basename_X, with X from 1 to +inf if not
	 * 
	 * @param elem
	 *            the element we want to name
	 * @param baseName
	 *            the base of the name to use
	 */
	protected void setGridElementName(GridElement elem, String baseName) {
		baseName = getBaseName(baseName);
		int x = 0;
		while (true) {
			try {
				String tryingName = numberedName(baseName, x);
				if (geFactory.getInGlobals(VMElementType.ANY, tryingName) == null) {
					elem.setName(tryingName);
					return;
				}
			} catch (ConfigurationException e) {
			} catch (RemoteException e) {
				throw new RuntimeException("TODO : handle this better", e);
			}
			x++;
		}

	}

	/**
	 * copy a GRack in another one.
	 * 
	 * @return a copy of the given rack, positionned at given position if there
	 *         is no rack preventing it ; If there is contact problem, then the
	 *         rack is moved to another close place<br />
	 *         The copied GRack is given the name of the source+"_X", X being 1
	 *         and increased by 1 while there is one Grack with that name.
	 *         Servers in the given rack are copied by the same process( same
	 *         position, same characteristics)
	 * @param toCopy
	 *            the rack we want to copy
	 */
	public GRack copyGRack(GRack toCopy) {
		try {
			Rack copiedRack = geFactory.copyRack(toCopy.getGridElement());
			setGridElementName(copiedRack, toCopy.getName());
			registerNewRack(copiedRack);
			GRack ret = generateGRack(copiedRack);
			copyServers(toCopy, ret);
			return ret;
		} catch (Exception e) {
			throw new RuntimeException("TODO : catch this better", e);
		}
	}

	/** clones each server in from and add the clone in to, at the same level */
	protected void copyServers(GRack from, GRack to) {
		to.setLvlNb(from.getLvlNb());
		GServer lastServer = null;
		for (int i = 0; i < from.getLvlNb(); i++) {
			GServer server = from.serverAtLvl(i);
			if (lastServer != server && server != null) {
				GServer clone = copyGServer(server);
				clone.removeFromRack();
				to.putServer(clone, i);
			}
			lastServer = server;
		}
	}

	/** copy a server and add it to the home */
	public GServer copyGServer(GServer toCopy) {
		try {
			Machine copiedMachine = geFactory.copyMachine(toCopy
					.getGridElement());
			setGridElementName(copiedMachine, toCopy.getName());
			GServer ret = generateGServer(copiedMachine);
			ret.setMac(createCorrectServerMac());
			ret.setIP(createCorrectServerIp());
			return ret;
		} catch (Exception e) {
			throw new RuntimeException("TODO : handle this better", e);
		}
	}

	/** create a new model with random name in the grid and return it */
	public MachineModel generateModel() {
		try {
			MachineModel ret = geFactory.newModel(null);
			grid.addMachineModel(ret);
			return ret;
		} catch (RemoteException e) {
			e.printStackTrace();
		} catch (ConfigurationException e) {
			e.printStackTrace();
		}
		return null;
	}
}
